package com.me.waterphase;

import com.me.waterphase.WaterAction.ACTION_TYPE;
import com.me.waterphase.listener.*;
import com.me.waterphase.listener.contact.EdgeContact;
import com.me.waterphase.listener.contact.FootContact;
import com.me.waterphase.listener.contact.ICustomContact;
import com.me.waterphase.listener.contact.PlayerContact;

import java.awt.peer.LightweightPeer;
import java.io.Console;
import java.util.ArrayList;
import java.util.Iterator;

import javax.naming.spi.Resolver;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.Fixture;
import com.badlogic.gdx.physics.box2d.BodyDef.BodyType;
import com.me.waterphase.utils.Conversion;
import com.me.waterphase.utils.PhysicManager;

public class WaterPlayer {

	//Constants.
	private final float SPEED_X = 0.75f;
	private final float JUMP_HEIGHT = 5f;
	private final float MAX_SPEED_FACTOR = 25.75f;
	
	private final float TURN_SLOWDOWN_FACTOR = 0.7f;
	private final float NOKEY_PRESSED_SLOWDOWN_FACTOR = 0.85f; 
	private final float ADDITIONAL_HUMAN_FRICTION = 0.93f;
	private final float ADDITIONAL_DROPPLET_FRICTION = 0.95f;
	
	private final float MAX_GRAVITY_SCALE = 2f;
	private final float MIN_GRAVITY_SCALE = -0.01f;
	private final float LOW_TEMPERATURE_LINE = 10;
	private final float HIGH_TEMPERATURE_LINE = 80;
	private final float LOW_TEMPERATURE_FACTOR = (MAX_GRAVITY_SCALE - 1)*0.01f*LOW_TEMPERATURE_LINE;
	private final float HIGH_TEMPERATURE_FACTOR = 1/(100-HIGH_TEMPERATURE_LINE);
	
	//Player stuff.
	private WaterArtifact artifact;
	private float width;
	private float height;
	private float temperature; //In celsius.
	
	//Animation stuff.
	private WaterAnimation anim;
	
	//Physic stuff.
	private Body humanBody;
	private Body dropBody;
	private Body form;
	private boolean isGravityDisabled;
	
	//Indexes.
	private int footIndex;
	private int edgeUpperPartIndex;
	private int edgeLowerPartIndex;
	
	//Action stuff.
	private WaterAction action;
	private boolean isTriggerable;
	
	//Usefull custom stuff.
	private ArrayList<Integer> pressedKeys;
	private FACING currentFacing;
	private FACING lastFacing;
	
	private PLAYER_STATE state;
	private PLAYER_STATE lastState;
	
	//Enum listing the player's states.
	public static enum PLAYER_STATE
	{
		DROPLET(0),
		LIGHT(1),
		MEDIUM(2),
		HEAVY(3),
		FROZEN(4);
		
		private int type;
		
		private PLAYER_STATE(int _type)
		{
			type = _type;			
		}
		
		public int Type()
		{
			return type;
		}
	}
	public static enum FACING
	{
		LEFT(1),
		RIGHT(2);
		
		private int direction;
		
		private FACING(int _direction)
		{
			direction = _direction;
		}
		
		public int Direction()
		{
			return direction;
		}
	}
	
	/*
	 * Constructors.
	 */
	
	public WaterPlayer(String _animationName, float _posX, float _posY, float _width, float _height, PLAYER_STATE _state, float _celsius){
		anim = new WaterAnimation(_animationName, _width, _height);
		width = _width;
		height = _height;
		state = _state;
		lastState = _state;
		
		action = null;
		isTriggerable = true;
		currentFacing = FACING.RIGHT;
		
		artifact = null;
		temperature = _celsius;
		isGravityDisabled = false;
		
		footIndex = -1;
		edgeLowerPartIndex = -1;
		edgeUpperPartIndex = -1;
		
		pressedKeys = new ArrayList<Integer>();
		
		//Create the physical actor for the player.
		humanBody = PhysicManager.createCapsuleBody(BodyType.DynamicBody, _posX, _posY, 0f, 35.0f, 0f, _width*0.75f, _height, true, true, true);
		
		dropBody = PhysicManager.createCircleBody(BodyType.DynamicBody, _posX, _posY, 0f, 35.0f, 0f, _width*0.75f, true, false);
		dropBody.setActive(false);
		
		form = humanBody;
		
		//Indicate to all the fixture that not already have a user data that they define the player.
		for(Iterator<Fixture> it = humanBody.getFixtureList().iterator(); it.hasNext();)
		{
			Fixture fixt = it.next();
			if(fixt.getUserData() == null)
				fixt.setUserData(new PlayerContact());
		}
		
		for(Iterator<Fixture> it = dropBody.getFixtureList().iterator(); it.hasNext();)
		{
			Fixture fixt = it.next();
			if(fixt.getUserData() == null)
				fixt.setUserData(new PlayerContact());
		}
	}
	
	/*
	 * Render the player.
	 */
	public void render(float _stateTime){
		
		float posX = Conversion.boxToWorld(form.getPosition().x)-(width*0.5f);
		float posY = Conversion.boxToWorld(form.getPosition().y)-(height*0.5f);
		
		anim.render(_stateTime, posX, posY);
	}
	
	public void dispose()
	{
		anim.dispose();
	}
	
	/*
	 * Manage the inputs and determine possible actions. Treatment pre-update of the physic world. 
	 */
	public void preUpdate()
	{	
		resolveTemperature();
		resolveGravity();
		
		//Check if player has triggered an action.
		determineAction();
		
		//Handle inputs.
		manageInput();
		
		//If a possible action hasn't been broken, resolve it.
		resolveAction();
	}
	
	/*
	 * Treatment post-update of the physic world.
	 */
	public void postUpdate(){
		
		//Pull the artifact toward the player center, if handled by the player.
		if(artifact != null)
		{
			if(form.getJointList().size() == 0){
				//float maxLength = (width*0.5f-artifact.getWidth()*0.5f)*0.33f;
				PhysicManager.createRopeJoint(0, form, artifact.getBody(), new Vector2(0,0), new Vector2(0,0));
			}
		}
		else
		{
			//If player has lost the artifact, break the joint between them.
			if(form.getJointList().size() > 0)
				PhysicManager.getInstance().destroyJoint(form.getJointList().get(0).joint);
		}
	}
	
	/*
	 * Change state of the player.
	 */
	public void changeState(PLAYER_STATE _state)
	{
		if(state != _state)
		{
			//Interrupt any action.
			if(action != null)
				action = null;
			
			//Transfer the current velocity to the other form.
			if(_state == PLAYER_STATE.DROPLET)
			{
				dropBody.setLinearVelocity(humanBody.getLinearVelocity());
				dropBody.setTransform(humanBody.getPosition(), 0f);
			}
			else if(state == PLAYER_STATE.DROPLET)
			{
				humanBody.setLinearVelocity(dropBody.getLinearVelocity());
				humanBody.setTransform(new Vector2(dropBody.getPosition().x, dropBody.getPosition().y), 0f);
			}
			
			lastState = state;
			state = _state;
			footIndex = -1;
			
			//Switch between the two different bodies.
			switch(state)
			{
				case DROPLET:
					humanBody.setActive(false);
					dropBody.setActive(true);
					
					form = dropBody;
					break;
				case LIGHT:
				case MEDIUM:
				case HEAVY:
					dropBody.setActive(false);
					humanBody.setActive(true);
					
					form = humanBody;
					break;
				case FROZEN:
					//TODO: Froze state process.
					break;
			}
			
			//Reset index on form change.
			if(_state == PLAYER_STATE.DROPLET || lastState == PLAYER_STATE.DROPLET)
			{
				//Reset indexes.
				footIndex = -1;
				edgeUpperPartIndex = -1;
				edgeLowerPartIndex = -1;
				
				//Reset number of contacts.
				getFootContact().setNumberFootContact(0);
				
				if(getEdgeUpperPart() != null)
				{
					getEdgeUpperPart().setNumberContact(0);
					getEdgeLowerPart().setNumberContact(0);
				}
				
				if(artifact != null)
					artifact.getArtifactContact().setNumberContact(0);
			}
		}
	}
	
	/*
	 * Method managing the player's inputs.
	 */
	public void move(){
		
		if(action == null || action.allowMovement()){
			
			if(state != PLAYER_STATE.DROPLET && state != PLAYER_STATE.FROZEN)
			{
				//Cancel the action if it's breakable on movement.
				if(action != null && action.isBreakable())
					action = null;
								
				float nextX = 0.0f;
				
				float slowingFactorX = 1f;
				float slowingFactorY = 1f;
				
				float factor = MAX_SPEED_FACTOR*Math.abs(form.getLinearVelocity().x)*(getFootContact().getNumberContact() > 0 ? 1 : 5);
				
				Vector2 direction = getFootContact().getDirection().cpy();
				
				//In case of an empty vector, create the default one.
				if(direction.x == 0 && direction.y == 0)
				{
					direction.x = 1;
					direction.y = 0;
				}
				
				//Manage the inputs.
				//Left input.
				if(Gdx.input.isKeyPressed(Keys.LEFT))
				{
					if(!Gdx.input.isKeyPressed(Keys.RIGHT))
						currentFacing = FACING.LEFT;
	
					nextX -= (1 > factor ? SPEED_X : SPEED_X/factor);
					
					if(getFootContact().getNumberContact() > 0)
					{
						//If the player is running up a slope, push him a bit up, otherwise, bring him down.
						if(direction.y < 0)
						{
							//Prevent player to climb a cliff.
							if(Math.abs(direction.y) < Math.abs(direction.x))
								direction.y *= nextX*0.15f;
							else
							{
								direction.x = 1;
								direction.y = 0;
							}
						}
						else
							direction.y *= nextX*1.5f;
					}
				}
				
				//Right input.
				if(Gdx.input.isKeyPressed(Keys.RIGHT))
				{
					if(!Gdx.input.isKeyPressed(Keys.LEFT))
						currentFacing = FACING.RIGHT;
					
					nextX += (1 > factor ? SPEED_X : SPEED_X/factor);
					
					if(getFootContact().getNumberContact() > 0)
					{
						//If the player is running down a slope, bring him down, otherwise, push him a bit up.
						if(direction.y < 0)
							direction.y *= nextX*1.5f;
						else
						{
							//Prevent player to climb a cliff.
							if(Math.abs(direction.y) < Math.abs(direction.x))
								direction.y *= nextX*0.15f;
							else
							{
								direction.x = 1;
								direction.y = 0;
							}
						}
					}
				}
				
				//If the player is turning, counter is current velocity to help him turn faster.
				if(currentFacing != lastFacing)
					turn();
						
				//Player is on the ground.
				if(getFootContact().getNumberContact() > 0)
				{
					isTriggerable = true;
					slowingFactorX *= ADDITIONAL_HUMAN_FRICTION;
					
					//Remove gravity on the player when he's standing on the ground to prevent him to slide down a slope.
					if(!isGravityDisabled)
						changeGravity(0f); 
					
					//If no key is pressed, prevent player to slide on a slope or to gain speed.
					if(nextX == 0)
					{
						slowingFactorX *= NOKEY_PRESSED_SLOWDOWN_FACTOR;
						slowingFactorY *= NOKEY_PRESSED_SLOWDOWN_FACTOR;
							
						direction.x = 0;
						direction.y = 0;
					}
					else
					{
						//Modify the impulse if the temperature is in the range of extremes.
						if(temperature < LOW_TEMPERATURE_LINE)
							nextX /= getTemperatureFactor()*2;
						else if(temperature > HIGH_TEMPERATURE_LINE)
						{
							float tempFactor = getTemperatureFactor();
							nextX *= (tempFactor < 0 ? 0 : tempFactor);
						}
					}
					
					//Jump.
					if(Gdx.input.isKeyPressed(Keys.SPACE)){
						 					
						if(!pressedKeys.contains(Keys.SPACE))
						{
							slowingFactorY = 0;
							
							jump(direction);
							pressedKeys.add(Keys.SPACE);
						}
					}
					else
					{
						if(pressedKeys.contains(Keys.SPACE))
							removeKey(Keys.SPACE);
					}

				}
				else
				{
					//Reset gravity.
					if(isGravityDisabled)
						changeGravity(1f);
					
					direction.x = 0;
					direction.y = 0;
				}
				
				//Obtain the velocity of the player.
				direction.x = Math.abs(direction.x)*nextX;
				
				//Apply a reduction of his linear velocity to slow him down faster than the usual friction.
				if(slowingFactorX != 0 || slowingFactorY != 0)
					form.setLinearVelocity(new Vector2(form.getLinearVelocity().x*slowingFactorX, form.getLinearVelocity().y*slowingFactorY));
				
				if(direction.x != 0 || direction.y != 0)
					form.applyForceToCenter(direction);
			}
			else
			{
				//Reset gravity.
				if(isGravityDisabled)
					changeGravity(1f);
				
				//Apply a custom friction on the drop to prevent it to roll forever.
				if(getFootContact().getNumberContact() > 0)
					form.setLinearVelocity(new Vector2(form.getLinearVelocity().x*ADDITIONAL_DROPPLET_FRICTION, form.getLinearVelocity().y));
			}
		}
		else if(action != null)
		{
			//Indicate if the player is affected by gravity during the action.
			if(!action.isGravitySensitive())
			{
				if(!isGravityDisabled)
					changeGravity(0f);
			}
			else
			{
				if(isGravityDisabled)
					changeGravity(1f);
			}
			
			//Indicate if the player can break the action by jumping.
			if(action.isBreakable())
			{
				//Jump.
				if(Gdx.input.isKeyPressed(Keys.SPACE)){
					 		
					if(!pressedKeys.contains(Keys.SPACE))
					{
						Vector2 direction = new Vector2(0,0);
						
						jump(direction);
						
						pressedKeys.add(Keys.SPACE);
						
						form.applyForceToCenter(direction);
						action = null;
					}
				}
				else
				{
					if(pressedKeys.contains(Keys.SPACE))
						removeKey(Keys.SPACE);
				}
			}
		}
		else
			//Reset gravity.
			if(isGravityDisabled)
				changeGravity(1f);
			
		//Keep current frame facing for movement.
		if(currentFacing != lastFacing)
			lastFacing = currentFacing;
	}
	
	/*
	 * Called when the player turns. 
	 */
	private void turn()
	{
		//Help the player to turn swifter.
		if(getFootContact().getNumberContact() > 0)
			form.setLinearVelocity(new Vector2(form.getLinearVelocity().x*TURN_SLOWDOWN_FACTOR, humanBody.getLinearVelocity().y*TURN_SLOWDOWN_FACTOR));
		else
			form.setLinearVelocity(new Vector2(form.getLinearVelocity().x*-1, humanBody.getLinearVelocity().y));

		edgeUpperPartIndex = -1;
		edgeLowerPartIndex = -1;
	}
	
	/*
	 * Used to change the gravity scale on the form. Eventually, the factor will be multiplied by a temperature factor.
	 */
	private void changeGravity(float _factor)
	{
		if(_factor == 0)
		{
			float tempFactor = getTemperatureFactor();
			if(tempFactor < 0)
				form.setGravityScale(tempFactor);
			else
				form.setGravityScale(0);
			
			isGravityDisabled = true;
		}
		else
		{
			isGravityDisabled = false;
			form.setGravityScale(_factor*getTemperatureFactor());
		}
	}
	
	/*
	 * Obtain a factor related to the player's temperature.
	 */
	private float getTemperatureFactor()
	{
		float factor = 1;
		
		if(temperature < LOW_TEMPERATURE_LINE)
		{
			if(temperature < 0)
				factor = MAX_GRAVITY_SCALE;
			else
			{
				factor = 1+((LOW_TEMPERATURE_LINE-temperature)*LOW_TEMPERATURE_FACTOR);
			}
				
		}
		else if(temperature > HIGH_TEMPERATURE_LINE)
		{
			if(temperature > 100)
				factor = MIN_GRAVITY_SCALE;
			else
				factor -= (temperature-HIGH_TEMPERATURE_LINE)*HIGH_TEMPERATURE_FACTOR;
		}
		
		return factor;
	}
	
	/*
	 * Handle player's inputs.
	 */
	private void manageInput()
	{
		//Allow movement and change state on the player if he's not frozen.
		if((action == null || action.isBreakable()) 
		    && state != PLAYER_STATE.FROZEN)
		{
			if(Gdx.input.isKeyPressed(Keys.Q))
				changeState(PLAYER_STATE.DROPLET);
			if(Gdx.input.isKeyPressed(Keys.A))
				changeState(PLAYER_STATE.MEDIUM);
		}
		
		if(action == null || action.allowMovement() || action.isBreakable())
			move();
	}
	
	/*
	 * Process new gravity for the player.
	 */
	private void resolveGravity()
	{
		if(!isGravityDisabled)
			changeGravity(1f);
		else
			changeGravity(0);
	}
	
	/*
	 * Process done related to the temperature.
	 */
	private void resolveTemperature()
	{
		if(temperature <= 0)
		{
			if(state != PLAYER_STATE.FROZEN)
				changeState(PLAYER_STATE.FROZEN);
		}
		else
		{
			if(state == PLAYER_STATE.FROZEN)
				changeState(lastState);
		}
			
	}
	
	/*
	 * Called when the player jumps.
	 */
	private void jump(Vector2 _vector)
	{
		if(_vector.y < JUMP_HEIGHT)
		{
			if(temperature < LOW_TEMPERATURE_LINE)
			{
				_vector.y = JUMP_HEIGHT/getTemperatureFactor();
			}
			else if (temperature > HIGH_TEMPERATURE_LINE)
			{
				float tempFactor = getTemperatureFactor()*2;
				_vector.y = JUMP_HEIGHT*(tempFactor > 1 ? 1 : tempFactor);
			}
			else
				_vector.y = JUMP_HEIGHT;
		}
			
	}
	
	/*
	 * Remove a specific key from the key pressed array.
	 */
	private void removeKey(int _key)
	{
		int i = 0;
		boolean found = false;
		while (i < pressedKeys.size() && !found)
		{
			if(pressedKeys.get(i) == _key)
				found = true;
			else
				i++;
		}
		
		if(found)
			pressedKeys.remove(i);
	}
	
	/*
	 * Determine if the player has triggered an action due to his situation.
	 */
	private void determineAction()
	{
		if(getEdgeUpperPart() != null)
		{
			//TODO: Limit to a certain amount of speed.
			if(getEdgeUpperPart().getNumberContact() == 0 && getEdgeLowerPart().getNumberContact() > 0 && isTriggerable)
			{
				action = WaterAction.create(ACTION_TYPE.CLIMB);
				isTriggerable = false;	
			}
		}
	}
	
	/*
	 * If the player is involved in any action, trigger it.
	 */
	private void resolveAction()
	{
		if(action != null)
		{
			switch(action.getType())
			{
				case CLIMB:
					
					if(action.getActionRemainingTime() > 0 || action.getAnimRemaingTime() > 0)
					{
						if(action.getActionDuration() == action.getActionRemainingTime())
							form.setLinearVelocity(0, 0);
							
						form.setLinearVelocity(0.01f*(currentFacing == FACING.RIGHT ? 1 : -1), 0.06f);
						action.update(Gdx.graphics.getDeltaTime());	
					}
					else
					{
						form.setLinearVelocity(0.03f*(currentFacing == FACING.RIGHT ? 1 : -1), 0.05f);
						action = null;
					}
					
					break;
			}
		}
	}
	
	
	/*
	 * Getters and setters.
	 */
	public WaterAnimation getAnimation(){return anim;}
	public void setAnimation(WaterAnimation _anim){anim = _anim;}
	
	public float getWidth(){return width;}
	public float getHeight(){return height;}
	public FACING getFacing(){return currentFacing;}
	public Body getBody(){return form;}
	public boolean isGravityDisabled(){ return isGravityDisabled;}
	public PLAYER_STATE getState(){ return state;}
	
	public WaterArtifact getArtifact(){return artifact;}
	public void setArtifact(WaterArtifact _artifact){artifact = _artifact;}
	
	public float getTemperature(){return temperature;}
	public void setTemperature(float _celsius){ temperature = _celsius;}
	
	public Vector2 getPosition(){
		return new Vector2(Conversion.boxToWorld(form.getPosition().x), Conversion.boxToWorld(form.getPosition().y));
	}
	
	public void setPosition(Vector2 _position){
		form.setTransform(_position, 0f);
	}
			
	public Vector2 getCenter()
	{
		return new Vector2(Conversion.boxToWorld(form.getPosition().x), 
						   Conversion.boxToWorld(form.getPosition().y));
	}
	
	public FootContact getFootContact(){
		
		FootContact currentFootContact = null;
		
		//Only seek for the footContact's index the first time. Afterward, directly access it.
		if(footIndex < 0)
		{
			boolean found = false;
			int i = 0;
			
			//Seek for the fixture having the foot contact.
			while( i < form.getFixtureList().size() && !found)
			{
				if(form.getFixtureList().get(i).getUserData() != null && ((ICustomContact)form.getFixtureList().get(i).getUserData()).getContactType() == PhysicManager.CONTACT_TYPE.USERDATA_GROUND_SENSOR)
				{
					currentFootContact = (FootContact)form.getFixtureList().get(i).getUserData();
					found = true;
					footIndex = i;
				}
				
				i++;
			}
		}
		else
			currentFootContact = (FootContact)form.getFixtureList().get(footIndex).getUserData();
		
		return currentFootContact;

	}
		
	public EdgeContact getEdgeUpperPart(){
		
		EdgeContact currentEdgeContact = null;
		
		if(edgeUpperPartIndex < 0)
		{
			boolean found = false;
			int i = 0;
			EdgeContact ec = null;
			
			//Seek for the fixture having the edge contact.
			while( i < form.getFixtureList().size() && !found)
			{
				if(form.getFixtureList().get(i).getUserData() != null && ((ICustomContact)form.getFixtureList().get(i).getUserData()).getContactType() == PhysicManager.CONTACT_TYPE.USERDATA_EDGE_SENSOR)
				{
					ec = ((EdgeContact)form.getFixtureList().get(i).getUserData());
					
					if(ec.isUpper()&& ec.getFacing() == currentFacing)
					{
						currentEdgeContact = ec;
						found = true;
						edgeUpperPartIndex = i;
					}
				}
				
				i++;
			}
		}
		else
			currentEdgeContact = (EdgeContact)form.getFixtureList().get(edgeUpperPartIndex).getUserData();
			
		return currentEdgeContact;
	}
	
	public EdgeContact getEdgeLowerPart(){
		
		EdgeContact currentEdgeContact = null;
		
		if(edgeLowerPartIndex < 0)
		{
			boolean found = false;
			int i = 0;
			EdgeContact ec = null;
			
			//Seek for the fixture having the edge contact.
			while( i < form.getFixtureList().size() && !found)
			{
				if(form.getFixtureList().get(i).getUserData() != null && ((ICustomContact)form.getFixtureList().get(i).getUserData()).getContactType() == PhysicManager.CONTACT_TYPE.USERDATA_EDGE_SENSOR)
				{
					ec = ((EdgeContact)form.getFixtureList().get(i).getUserData());
					if(!ec.isUpper()&& ec.getFacing() == currentFacing)
					{
						currentEdgeContact = (EdgeContact)form.getFixtureList().get(i).getUserData();
						found = true;
						edgeLowerPartIndex = i;
					}
				}
				
				i++;
			}
		}
		else
			currentEdgeContact = (EdgeContact)form.getFixtureList().get(edgeLowerPartIndex).getUserData();
			
		return currentEdgeContact;
	}
	
}
